package org.springframework.batch.item.file; import be.raildelays.batch.AbstractFileTest; import be.raildelays.batch.bean.BatchExcelRow; import be.raildelays.batch.writer.ExcelRowAggregator; import be.raildelays.domain.Sens; import be.raildelays.domain.entities.Station; import be.raildelays.domain.entities.TrainLine; import be.raildelays.domain.xls.ExcelRow; import org.junit.*; import org.junit.runner.RunWith; import org.junit.runners.BlockJUnit4ClassRunner; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemStreamException; import org.springframework.batch.test.MetaDataInstanceFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.nio.channels.FileLock; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * @author Almex */ @RunWith(BlockJUnit4ClassRunner.class) public class ExcelSheetItemWriterTest extends AbstractFileTest { private ExcelSheetItemWriter<ExcelRow> writer; private List<ExcelRow> items = new ArrayList<>(); private ExecutionContext executionContext; @Before public void setUp() throws Exception { File directory = new File(CURRENT_PATH); if (!directory.exists()) { Assert.assertTrue("Cannot create a directory for the test", directory.mkdir()); } else { cleanUp(); } executionContext = MetaDataInstanceFactory.createStepExecution().getExecutionContext(); writer = new ExcelSheetItemWriter<>(); writer.setTemplate(new ClassPathResource("template.xls")); writer.setResource(new FileSystemResource(CURRENT_PATH + "output.xls")); writer.setRowAggregator(new ExcelRowAggregator()); writer.setName("test"); writer.setRowsToSkip(21); writer.setSheetIndex(0); writer.setShouldDeleteIfExists(false); writer.setUseItemIndex(true); writer.setCurrentItemIndex(0); writer.setMaxItemCount(40); writer.afterPropertiesSet(); items = new ArrayList<>(); List<LocalDate> dates = new ArrayList<>(80); for (int i = 0; i < 80; i++) { dates.add(LocalDate.now().minus(1, ChronoUnit.DAYS)); } for (LocalDate date : dates) { BatchExcelRow from = new BatchExcelRow.Builder(date, Sens.DEPARTURE) // .departureStation(new Station("Liège-Guillemins")) // .arrivalStation(new Station("Bruxelles-central")) // .expectedDepartureTime(LocalTime.parse("08:00")) // .expectedArrivalTime(LocalTime.parse("09:00")) // .expectedTrain1(new TrainLine.Builder(466L).build()) // .effectiveDepartureTime(LocalTime.parse("08:05")) // .effectiveArrivalTime(LocalTime.parse("09:15")) // .effectiveTrain1(new TrainLine.Builder(466L).build()) // .build(); BatchExcelRow to = new BatchExcelRow.Builder(date, Sens.ARRIVAL) // .departureStation(new Station("Bruxelles-central")) // .arrivalStation(new Station("Liège-Guillemins")) // .expectedDepartureTime(LocalTime.parse("14:00")) // .expectedArrivalTime(LocalTime.parse("15:00")) // .expectedTrain1(new TrainLine.Builder(529L).build()) // .effectiveDepartureTime(LocalTime.parse("14:05")) // .effectiveArrivalTime(LocalTime.parse("15:15")) // .effectiveTrain1(new TrainLine.Builder(529L).build()) // .build(); items.add(from); items.add(to); } } /** * We expect a normal execution with a template. */ @Test public void testTemplate() throws Exception { writer.open(executionContext); writer.write(items.subList(0, 2)); writer.update(executionContext); writer.close(); Assert.assertEquals(1, getExcelFiles().length); Assert.assertEquals(117248, getExcelFiles()[0].length()); } /** * We expect a normal execution with no template and in '.xls' format. */ @Test public void testNoTemplateOLE2() throws Exception { writer.setTemplate(null); writer.setResource(new FileSystemResource(CURRENT_PATH + "output.xls")); writer.open(executionContext); writer.update(executionContext); writer.close(); Assert.assertEquals(1, getExcelFiles().length); } /** * We expect a normal execution with no template and in '.xlsx' format. */ @Test public void testNoTemplateOOXML() throws Exception { writer.setTemplate(null); writer.setResource(new FileSystemResource(CURRENT_PATH + "output.xlsx")); writer.open(executionContext); writer.update(executionContext); writer.close(); Assert.assertEquals(1, getExcelFiles().length); } /** * We expect to read a file in the wrong format and get an InvalidFormatException embedded into an * ItemStreamException. */ @Test(expected = ItemStreamException.class) public void testInvalidFormatException() throws Exception { Path path = Paths.get(CURRENT_PATH, "output.dat"); // We create a non-empty file of 3 bytes try (OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) { outputStream.write(new byte[]{1, 2, 3}); } try { writer.setTemplate(null); writer.setResource(new FileSystemResource(path.toFile())); writer.open(executionContext); } finally { Files.deleteIfExists(path); } } /** * We expect to generate an IOException when calling open() which should embedded into an ItemStreamException. */ @Test(expected = ItemStreamException.class) public void testIOExceptionOnOpen() throws Exception { Path path = Paths.get(CURRENT_PATH, "output.dat"); try { writer.setTemplate(new FileSystemResource("test.dat")); writer.setResource(new FileSystemResource(path.toFile())); writer.open(executionContext); } finally { Files.deleteIfExists(path); } } /** * We expect to test the path where we delete an existing file on open(). */ @Test public void testShouldDeleteIfExists() throws Exception { Path path = Paths.get(CURRENT_PATH); Path file = Files.createTempFile(path, "output", ".xls"); Instant original = Files .readAttributes(file, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS) .creationTime() .toInstant(); Thread.sleep(1000); writer.setResource(new FileSystemResource(file.toFile())); writer.setShouldDeleteIfExists(true); writer.open(executionContext); writer.close(); Instant actual = Files .readAttributes(file, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS) .creationTime() .toInstant(); // Apply best effort pattern to delete the file at the end of the process try (OutputStream ignored = Files.newOutputStream(file, StandardOpenOption.DELETE_ON_CLOSE)) { Assert.assertNotEquals(original, actual); } } /** * We expect to write one file of 40 rows despite the fact that there are enough items to write two files. * As the maxItemCount=40 it should stop writing after 40 items. */ @Test public void testFileLimits() throws Exception { writer.open(executionContext); writer.write(items.subList(0, 10)); writer.update(executionContext); writer.write(items.subList(10, 20)); writer.update(executionContext); writer.write(items.subList(20, 30)); writer.update(executionContext); writer.write(items.subList(30, 40)); writer.update(executionContext); writer.write(items.subList(40, 80)); writer.update(executionContext); writer.close(); Assert.assertEquals(1, getExcelFiles().length); Assert.assertEquals(124416, getExcelFiles()[0].length()); } /** * We expect that upon a restart, the writer start from where it left. */ @Test public void testRestart() throws Exception { writer.open(executionContext); writer.write(items.subList(0, 10)); writer.update(executionContext); writer.close(); writer.open(executionContext); //-- By retrieving the same execution context it should be able to restart writer.write(items.subList(10, 40)); writer.update(executionContext); writer.close(); Assert.assertEquals(1, getExcelFiles().length); Assert.assertEquals(124416, getExcelFiles()[0].length()); } /** * We expect that an empty list fo items will not raise any error. */ @Test public void testEmptyList() throws Exception { writer.open(executionContext); writer.write(Collections.emptyList()); writer.update(executionContext); writer.close(); Assert.assertEquals(1, getExcelFiles().length); Assert.assertEquals(117248, getExcelFiles()[0].length()); } /** * We expect if we lock the file before calling close() we get an IOException embedded into an ItemStreamException */ @Test(expected = ItemStreamException.class) @Ignore // Not portable on Linux public void testIOExceptionOnClose() throws Exception { writer.open(executionContext); try (FileOutputStream outputStream = new FileOutputStream(Paths.get(CURRENT_PATH, "output.xls").toFile())) { try (FileLock ignored = outputStream.getChannel().lock()) { writer.close(); } } } @After public void tearDown() throws InterruptedException { // We must be sure that we close everything at the end of the test writer.close(); cleanUp(); } }